using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Html;
using System.Runtime.CompilerServices;
using System.Serialization;
using Awaiter;
using Canvas.Scripts.ImageRotate;
using Nsb.Classes;
using ThemeChain;
using jQueryApi;

namespace Minions.Classes
{
    [IgnoreNamespace]
    public class Minion : Actor
    {
        public const string MinionTypeKey = "gigType";
        public const string MinionTiltKey = "gigTilt";
        public const string MinionHasHeightKey = "gigHasHeight";
        public const string MinionClassName = "Gig";
        public const string DotMinionClassName = "." + MinionClassName;
        public const string MinionContentClassName = "GigC";
        public const string DotMinionContentClassName = "." + MinionContentClassName;
        private static readonly RegularExpression ExtractGigClassRx = new RegularExpression(@"<gigclass:\s*(.*)\s*/>", "g");

        private static readonly Dictionary<string, Minion> AllMinions = new Dictionary();
        public Dictionary<string, Minion> Children = new Dictionary();
        public jQueryObject Content;
        public string Id;
        public string Units = "px";
        protected bool Unsaved;
        private MinionData _data;
        private bool _editing;
        private jQueryObject _hostElement;
        private Minion _parent;
        private string _partitionKey;
        private string _rowKey;
        private MiCanvas _theCanvas;
        private Minion _topMinion;

        #region Configuration

        public static readonly Dictionary<string, MinionShowConfig> CssMap =
            new Dictionary(
                "Button", new Dictionary("inner", "button", "rounding", false, "css", true, "doubleclick", true),
                "TextBox", new Dictionary("inner", "input[type=text]", "rounding", true, "css", true, "doubleclick", true),
                "TextArea", new Dictionary("inner", "textarea", "rounding", true, "css", true, "doubleclick", true),
                "TextItem", new Dictionary("inner", DotMinionContentClassName, "rounding", true, "css", false, "doubleclick", true),
                "DropImage", new Dictionary("inner", "img", "rounding", true, "css", false, "doubleclick", false)
                );

        #endregion Configuration

        public Minion()
        {
            // ReSharper restore PossibleNullReferenceException
            Element = DefaultHtml;
            Element.Data(MinionClassName, this); // To make it instantly available to GetMinionByEl/Id - probably not really necessary
            _topMinion = this;
            _theCanvas = new MiCanvas(new MiCanvasData());
            _hostElement = jQuery.FromElement(Document.Body);
        }

        #region Finders

        public static Minion GetMinionById(string id)
        {
            try
            {
#if DEBUG
                Minion mm = AllMinions[id];
                if (mm == null)
                {
                    Script.Literal("debugger");
                    return null;
                }
                return mm;
#else
                return AllMinions[id] ?? null; // Force converts "undefiend" to "null"
#endif
            }
            catch (Exception)
            {
#if DEBUG
                Script.Literal("debugger");
#endif
                return null;
            }
        }

        #endregion Finders

        #region Properties

        private jQueryObject DefaultHtml
        {
            get { return jQuery.FromHtml("<div class='" + MinionClassName + "'/>"); }
        }

        public Minion TopMinion
        {
            get { return _topMinion; }
            protected set { _topMinion = value; }
        }

        public MiCanvas TheCanvas
        {
            get { return _topMinion == this ? _theCanvas : _topMinion.TheCanvas; }
        }

        public Minion Parent
        {
            get { return _parent; }
            set { _parent = value; }
        }

        public MinionData Data
        {
            get { return _data; }
            set
            {
                _data = value;
                if (_data != null)
                    Units = _data.relative ? "%" : "px";
            }
        }

        public MinionData LiveData
        {
            get
            {
                Data.minions = LiveChildData;
                if (!Data.relative)
                {
                    Data.offsetParentWidth = Element.OffsetParent().GetWidth();
                    Data.offsetParentHeight = Element.OffsetParent().GetHeight();
                }
                return Data;
            }
        }

        public bool Editing
        {
            get { return _topMinion == this ? _editing : TopMinion.Editing; }
            set
            {
                if (_topMinion == this) _editing = value;
                else _topMinion.Editing = value;
            }
        }

        public string PartitionKey
        {
            get
            {
                try
                {
                    return _partitionKey ?? _topMinion.PartitionKey;
                }
                catch
                {
                    return null;
                }
            }
        }

        public string RowKey
        {
            get
            {
                try
                {
                    return _rowKey ?? _topMinion.RowKey;
                }
                catch
                {
                    return null;
                }
            }
        }

        public Dictionary<string, MinionData> LiveChildData
        {
            get
            {
                Dictionary<string, MinionData> minions = new Dictionary<string, MinionData>();
                if (Children.Count > 0)
                    foreach (KeyValuePair<string, Minion> pair in Children)
                    {
                        minions[pair.Key] = pair.Value.LiveData;
                    }
                return minions;
            }
        }

        public jQueryObject HostElement
        {
            get { return _parent != null ? _parent.Element : _hostElement; }
        }

        public static int Count
        {
            get { return AllMinions.Count; }
        }

        public static Dictionary<string, Minion> All
        {
            get { return AllMinions; }
        }

        #endregion Properties

        #region Methods

        public void Destroy()
        {
            Element
                .RemoveData(MinionClassName)
                .RemoveData(MinionTypeKey)
                .RemoveData(MinionTiltKey)
                .RemoveData(MinionHasHeightKey)
                .Remove()
                ;
            Type.DeleteField(AllMinions, Id);
            if (Parent != null)
            {
                Type.DeleteField(Parent.Children, Id);
                Parent.Data = Parent.LiveData;
            }
            foreach (KeyValuePair<string, Minion> pair in Children)
            {
                pair.Value.Destroy();
            }
            Children = null;
            Data = null;
            Content = null;
            _parent = null;
            _editing = false;
        }

        protected virtual void Draggable()
        {
            // Virtual function only useful in MinionEdit
        }

        protected virtual void Droppable()
        {
            // Virtual function only useful in MinionEdit
        }

        public virtual Minion NewMinion(Minion top)
        {
            return new Minion().SetTop(top);
        }

        public virtual void DeleteMinion(bool immediate)
        {
            if (immediate)
                DeleteNow();
            else
                new Await()
                    .AddAw(new Ask().OkCancelAw, ThemeYoshi.ForSure)
                    .AddAw(RequireResultAw, true)
                    .AddDx(delegate { DeleteNow(); })
                    .Commit();
        }

        private void DeleteNow()
        {
            Destroy();
        }

        /// <summary>
        /// Apply css to gig adding other css stuff too (needs better doc)
        /// </summary>
        /// <param name="gd">gig data</param>
        /// <param name="el">page element</param>
        /// <param name="css">css to apply</param>
        public void MapCss(MinionData gd, jQueryObject el, Dictionary css)
        {
            css = OnTheFlyCss(css, el);
            MinionShowConfig opt = CssMap[gd.type];
            if (opt == null)
            {
                el.CSS(css);
                return;
            }
            jQueryObject sel = jQuery.Select(opt.inner, el);
            if (sel.Length > 0)
            {
                double bw = Css.BorderWidth(gd.css);
                if (opt.rounding)
                {
                    // Map outer radius onto inner content radius where specified by opt.rounding
                    double br = Number.ParseDouble((gd.css["border-radius"] ?? 0).ToString());
                    br = Math.Max(br - bw, 0);
                    string rds = Data.relative ? EmStr(br) : br + "px";
                    sel
                        .CSS(
                            new Dictionary(
                                "-moz-border-radius", rds,
                                "-webkit-border-radius", rds,
                                "border-radius", rds
                                ));
                }
                if (opt.css)
                {
                    double wd = Css.PaddingHorz(gd.css) + (bw*2);
                    double ht = Css.PaddingVert(gd.css) + (bw*2);
                    sel.CSS(css);
                    sel.CSS(new Dictionary("width", gd.dim.width - wd + Units, "height", gd.dim.height - ht + Units));
                }
                else
                    el.CSS(css);
            }
            else
                el.CSS(css);
        }

        public Minion AbsoluteParent()
        {
            Minion mm = this;
            while (mm != null && mm.Data.relative)
                mm = mm.Parent;
            return mm;
        }

        protected Dictionary OnTheFlyCss(Dictionary orgCss, jQueryObject el)
        {
            Dictionary css = new Dictionary();
            foreach (KeyValuePair<string, string> pair in orgCss)
            {
                double wfx = 100/Data.offsetParentWidth;
                Minion ap = AbsoluteParent();
                double apEm1 = (10/Number.ParseDouble(ap.Element.GetCSS("font-size") ?? "10px"))/10;
                //Inform.Debug("AbsoluteParent em = {0}, got from = {1}", apEm1, ap.Element.GetCSS("font-size"));
                if (Data.relative)
                {
                    if (((RegularExpression) Script.Literal("/^(padding|margin)/")).Test(pair.Key))
                        css[pair.Key] = Number.ParseDouble(pair.Value)*wfx + "%";
                    else if (pair.Key.IndexOf("-radius") != -1)
                        css[pair.Key] = Number.ParseDouble(pair.Value)*.1 + "em";
                    else if (pair.Key == "border-width")
                    {
                        double v = Number.ParseDouble(pair.Value)*.1;
                        css[pair.Key] = v < apEm1 ? ( /*apEm1 * .75 + "em"*/ "1px") : (v + "em");
                    }
                    else
                        css[pair.Key] = pair.Value;
                }
                else
                    css[pair.Key] = pair.Value;
            }
            if (Data.relative)
                css["font-size"] = Data.hasFont ? EmStr((string) css["font-size"] ?? "10px") : "1em";
            else
                css["font-size"] = Data.hasFont ? PxStr((string)css["font-size"] ?? "10px") : EmStr(Data.dim.width * .1);
            return css;
        }

        //public void PlaceMinion(Minion mm, jQueryObject deef, MinionData gd)
        //{
        //    // Handles putting minions in groups. No longer needed
        //    string aa = null;
        //    Minion gp = TopMinion.MinionById(gd.parentId);
        //    if (gp != null)
        //        aa = gp.Data.actions.place;

        //    if (aa != null)
        //        DoAction(aa, mm, gd);
        //    else
        //        mm.AppendTo(deef);
        //}

        public void DoAction(string action, object arg1, object arg2)
        {
            try
            {
                Action<object, object> fn = (Action<object, object>) Script.Eval(action.Trim());
                fn(arg1, arg2);
            }
            catch (Exception ex)
            {
                Inform.Error("Do action error: {0}: {1}", action, ex);
            }
        }

        private static void FixGd(MinionData md)
        {
            md.css = md.css ?? new Dictionary();
            md.floated = Script.IsValue(md.floated) && md.floated;
            md.embedded = Script.IsValue(md.embedded) && md.embedded;
        }

        public static jQueryObject ParseContent(string txt)
        {
            Script.Literal("{0}.lastIndex = 0", ExtractGigClassRx);
            string[] mx = ExtractGigClassRx.Exec(txt);
            if (mx != null && mx.Length > 1)
            {
                try
                {
                    return (jQueryObject) Script.Eval("new " + mx[1] + "().Content()");
                }
                catch (Exception ex)
                {
                    string s = ex.ToString();
                    return jQuery.FromHtml("<div>" + s + "</div>");
                }
            }
            return jQuery.FromHtml(txt);
        }

        public static string PxStr(string value)
        {
            if (value.IndexOf("pt") > 0)
            {
                return (Number.ParseDouble(value) * 1.3333) + "px";
            }
            if (value.IndexOf("em") > 0)
            {
                return (Number.ParseDouble(value) * 10) + "px";
            }
            return Number.ParseDouble(value) + "px";
        }

        public static string PtStr(string value)
        {
            return Pt(value) + "pt";
        }

        public static double Pt(string value)
        {
            if (value.IndexOf("pt") > 0)
            {
                return Number.ParseDouble(value);
            }
            if (value.IndexOf("em") > 0)
            {
                return Number.ParseDouble(value) * 7.5;
            }
            return Number.ParseDouble(value) * 0.75;
        }

        [AlternateSignature]
        public static extern string EmStr(double value);

        public static string EmStr(string value)
        {
            return Em(value) + "em";
        }

        [AlternateSignature]
        public static extern double Em(double value);

        public static double Em(string value)
        {
            try // shunts double values ot the default
            {
                if (value.IndexOf("em") > 0)
                {
                    return Number.ParseDouble(value);
                }
                if (value.IndexOf("pt") > 0)
                {
                    return Number.ParseDouble(value) * 0.13333;
                }
            }
            catch
            {
            }
            // For pixels or a plain number
            return Number.ParseDouble(value) * 0.1;
        }

        #endregion Methods

        #region Evokes

        public virtual void RePaint(Evo evo)
        {
            // WARNING: Distroys this instance.
            // Do not cache and resuse the instance after using RePaint.
            Minion mm = Clone();
            jQueryObject placeholder = jQuery.FromHtml("<div/>").InsertAfter(Element);
            Destroy();
            mm.AddTo(mm.Parent).Paint();
            mm.Element.InsertBefore(placeholder);
            placeholder.Remove();
        }

        public virtual void RePaintCanvas(Evo evo)
        {
            TopMinion.RePaint(evo);
        }

        public void Show(Evo evo)
        {
            Element.Show();
        }

        public void Hide(Evo evo)
        {
            Element.Hide();
        }

        #endregion Evokes

        #region Chains

        public Minion SetData(MinionData md)
        {
            Data = md;
            SetId(md.id ?? Formulas.NewId);
            return this;
        }

        public Minion SetTop(Minion mm)
        {
            Debug.Assert(Parent == null, "Parent must be null");
            TopMinion = mm;
            foreach (KeyValuePair<string, Minion> pair in Children)
            {
                pair.Value.SetTop(mm);
            }
            return this;
        }

        public Minion SetAsCanvas(MiCanvas cv)
        {
            Debug.Assert(Parent == null, "Parent must be null");
            _theCanvas = cv;
            SetTop(this);
            return this;
        }

        public Minion SetHost(jQueryObject el)
        {
            _hostElement = el;
            return this;
        }

        public Minion SetDb(string partitionKey, string rowKey)
        {
            _partitionKey = partitionKey;
            _rowKey = rowKey;
            return this;
        }

        public Minion AddTo(Minion parent)
        {
            Debug.Assert(!String.IsNullOrEmpty(Id) && Data != null, "has Data && Id");
            _parent = parent;
            if (parent != null)
            {
                parent.Children[Id] = this;
            }
            return this;
        }

        public Minion Clone()
        {
            Minion mm = NewMinion(TopMinion);
            mm
                .SetData((MinionData) Json.Parse(Json.Stringify(LiveData))) // Includes SetId()
                .SetHost(HostElement)
                .SetDb(_partitionKey, _rowKey)
                .SetTop(TopMinion == this ? mm : TopMinion)
                .SetAsCanvas(TheCanvas)
                ;
            mm._theCanvas = TheCanvas;
            return mm; // Parent will be null
        }

        public Minion LoadFrom(string partitionKey, string rowKey)
        {
            // TODO: Fill this in
            return this;
        }

        public Minion SetId(string id)
        {
            Id = id ?? Id;
            Debug.Assert(Data != null, "Data != null");
            // ReSharper disable PossibleNullReferenceException
            Data.id = Id;
            // ReSharper restore PossibleNullReferenceException
            Debug.Assert(Element != null, "Element != null");
            // ReSharper disable PossibleNullReferenceException
            Element.Attribute("id", Id);
            // ReSharper restore PossibleNullReferenceException
            return this;
        }

        [AlternateSignature]
        // ReSharper disable CSharpWarnings::CS0108
        public extern Minion AppendTo(string sel);

        // ReSharper restore CSharpWarnings::CS0108

        // ReSharper disable CSharpWarnings::CS0108
        public Minion AppendTo(jQueryObject el)
            // ReSharper restore CSharpWarnings::CS0108
        {
            Element.AppendTo(el);
            return this;
        }

        // ReSharper disable CSharpWarnings::CS0108
        public Minion AddClass(string cls)
            // ReSharper restore CSharpWarnings::CS0108
        {
            if (!String.IsNullOrEmpty(cls))
                Element.AddClass(cls);
            return this;
        }

        public Minion AddAttr(Dictionary attr)
        {
            if (attr != null && attr.Count > 0)
                Element.Attribute(attr);
            return this;
        }

        public Minion AddType(string type)
        {
            if (!String.IsNullOrEmpty(type))
                Element.Data(MinionTypeKey, type);
            return this;
        }

        // ReSharper disable CSharpWarnings::CS0108
        public Minion FromHtml(string html)
            // ReSharper restore CSharpWarnings::CS0108
        {
            Content = jQuery.FromHtml(html).PrependTo(Element);
            return this;
        }

        public Minion FromElement(jQueryObject el)
        {
            Content = el;
            el.PrependTo(Element);
            return this;
        }

        public Minion AddTilt(double deg)
        {
            Element.Data(MinionTiltKey, deg);
            Type.SetField(Element[0], "tilt", deg); // Mainly for SnapTo library
            Element.Plugin<IRot>().Rotate(new Dictionary("angle", deg));
            return this;
        }

        public Minion AddHasHeight(bool has)
        {
            Element.Data(MinionHasHeightKey, has);
            return this;
        }

        public Minion Paint()
        {
            InjectIntoPage();
            AllMinions[Id] = this;
            return this;
        }

        /// <summary>
        /// Workhorse builds passed Minion into a jQueryObect and inserts it into the page.
        /// </summary>
        /// <returns></returns>
        public Minion InjectIntoPage()
        {
            // ReSharper disable PossibleNullReferenceException
            Debug.Assert(Data != null, "Data != null");
            Element.Data(MinionClassName, this);
            SetId(Data.id);
            // ReSharper restore PossibleNullReferenceException
            FixGd(Data);
            // TODO: KLUDGE - what should this be now if anything?sd
            //CurrentSlide.minion[gd.id] = gd;
            jQueryObject where = HostElement;
            if (where.Length != 0)
            {
                jQueryObject content = ParseContent(Data.html);
                FromElement(content)
                    .AddClass(Data.classNames)
                    .AddAttr(Data.attributes)
                    .AddType(Data.type)
                    .AddHasHeight(Data.hasHeight)
                    ;
                jQueryObject iel = Element.Find(DotMinionContentClassName);
                if (iel.Length > 0)
                {
                    if (!String.IsNullOrEmpty(Data.iid))
                        iel.Attribute("id", Data.iid);
                    if (!String.IsNullOrEmpty(Data.iclass))
                        iel.AddClass(Data.iclass);
                }
                AppendTo(where);
                //PlaceMinion(this, where, Data);
                PaintMetrics(true);
                //Dictionary css = Data.css;
                //if (Editing != null)
                //    css = Editing.Configure(Data, this);
                //MapCss(Data, Element, css);
                AddTilt(Data.tilt);
                Draggable();
                Droppable();
                //if (Data.selecTo != PageSelectTo)
                //    Element.Hide();

                // Paint children
                if (Data.minions != null)
                    foreach (KeyValuePair<string, MinionData> pair in Data.minions)
                    {
                        NewMinion(TopMinion).SetData(pair.Value).AddTo(this).Paint();
                    }
            }
            else
            {
                throw new Exception(Exceptions.Format(Exceptions.MissingDomElement, Exceptions.Fn(Script.Literal("arguments"))));
            }
            return this;
        }

        [AlternateSignature]
        public extern Minion PaintMetrics();

        public Minion PaintMetrics(bool noChildren)
        {
            string units = Data.relative ? "em" : Units;
            double tp = Data.pos.Top;
            double lf = Data.pos.Left;
            double wd = Data.dim.width;
            double ht = Data.dim.height;

            if (Data.relative)
            {
                tp *= .1;
                lf *= .1;
                wd *= .1;
                ht *= .1;
            }

            Element.CSS(new Dictionary("position", "absolute", "top", tp + units, "left", lf + units, "width", wd + units));
            if (Data.hasHeight)
                Element.CSS(new Dictionary("height", ht + units));
            MapCss(Data, Element, Data.css);
            Inform.Debug("PaintMetrics id --------- {0} ------------------------------------------", Id);
            Inform.Debug("PaintMetrics units=em, tp={0}, lf={1}, wd={2}, ht={3}", tp, lf, wd, ht);
            Inform.Debug("PaintMetrics units=px, tp={0}, lf={1}, wd={2}, ht={3}", Element.Position().Top, Element.Position().Left, Element.GetWidth(), Element.GetHeight());
            if (!noChildren)
                foreach (KeyValuePair<string, Minion> pair in Children)
                {
                    pair.Value.PaintMetrics();
                }
            return this;
        }

        public Minion PaintMetricsPercent(bool noChildren)
        {
            double tp = Data.pos.Top;
            double lf = Data.pos.Left;
            double wd = Data.dim.width;
            double ht = Data.dim.height;

            if (Data.relative)
            {
                double fh = 100 / Data.offsetParentWidth;
                double fv = 100 / Data.offsetParentHeight;
                tp *= fv;
                lf *= fh;
                wd *= fh;
                ht *= fv;
            }

            Element.CSS(new Dictionary("position", "absolute", "top", tp + Units, "left", lf + Units, "width", wd + Units));
            if (Data.hasHeight)
                Element.CSS(new Dictionary("height", ht + Units));
            MapCss(Data, Element, Data.css);
            if (!noChildren)
                foreach (KeyValuePair<string, Minion> pair in Children)
                {
                    pair.Value.PaintMetrics();
                }
            return this;
        }

        #endregion Chains
    }

    #region Classes
    public sealed class MinionData
    {
        // ReSharper disable InconsistentNaming
        public MinionActions actions;
        public Dictionary attributes;
        public bool autosize;
        public string build;
        public string classNames = "";
        public string clusterId;
        public int cornerRadius;
        public Dictionary css = new Dictionary();
        public Dictionary<string, object> data;
        public Dimension dim = new Dimension();
        public bool embedded;
        public bool floated;
        public string groupId;
        public bool hasHeight;
        public Dictionary<string, string> help;
        public string html;
        public string iclass;
        public string id = Formulas.NewId;
        public string iid;
        public string listType;
        public string load;
        public Dictionary<string, MinionData> minions;
        public bool noCover;
        public double offsetParentWidth;                    // Used to calculate relative values. Frozen when absolute minion is converted to relative.
        public double offsetParentHeight;                   // Used to calculate relative values. Frozen when absolute minion is converted to relative.
        public string parentId;
        public Pos pos = new Pos();
        public bool relative;
        public string selecTo;
        public double tilt;
        public string type;
        public bool hasFont;                                // Has a font with a size so apply font/width factor to children not this.
        // ReSharper restore InconsistentNaming
    }

    public sealed class MinionActions
    {
        public string paste;
        public string place;
    }

    public sealed class Pos
    {
        // ReSharper disable InconsistentNaming
        public double Left;
        public double Top;
        // ReSharper restore InconsistentNaming

        public static Pos Clone(Pos p)
        {
            Pos pos = new Pos();
            pos.Top = p.Top;
            pos.Left = p.Left;
            return pos;
        }
    }

    public sealed class Dimension
    {
        // ReSharper disable InconsistentNaming
        public double height;
        public double width;
        // ReSharper restore InconsistentNaming
    }

    public abstract class MinionShowConfig
    {
        public bool css;
        public bool doubleclick;
        public string inner;
        public bool rounding;
    }

    #endregion Classes

}